(function (root, factory) {
	"use strict";

	if (typeof define === 'function' && define.amd) {
		// Set up jQuery.Scroolly appropriately for the environment. Start with AMD.
		define(['jquery'], function ($) {
			// Export global even in AMD case in case this script is loaded with
			return factory(root, $, false);
		});

		return;
	}

	// Finally, as a browser global in jquery ns.
	factory(root, (root.jQuery || root.Zepto || root.ender || root.$), true);
}(this, function (root, $, patchJQuery) {
	"use strict";

	var scroolly;

	scroolly = {
		options: {
			timeout: null,
			meter: $('.scroolly'),
			body: document
		},
		theCSSPrefix: '',
		theDashedCSSPrefix: '',
		isMobile: false,
		isInitialized: false,
		//        requestAnimFrame: null,
		//        cancelAnimFrame: null,

		animFrame: null,
		direction: 0,
		scrollTop: 0,
		scrollCenter: 0,
		scrollBottom: 0,
		docHeight: 0,
		docMiddle: 0,
		winHeight: $(window).height()
	};

	scroolly.scrollLayout = {
		//  TSB - top screen border
		//        topbarSearchForm:{
		//            element: searchFormTop,
		//            rules:[
		//                {
		//                    from: 0, // top border of the rule region
		//                    to: 'finish', // bottom border of the rule region
		//                          // if ommited then set to 'from' of the following rule
		//                          // if there is no following rule set to 'bottom'
		//                    minWith: 0, // min viewport width for the rule to apply
		//                    maxWidth: 'infinity', // max viewport width for the rule to apply
		//                    direction: 0, // 0 - ignored, >0 - forward, <0 - backward
		//                    alias: 'top', // region alias
		//                    css: null,//{'display': 'none'}, // css to apply when TSB enters rule region
		//                    cssFrom: {'border': '0px solid #000000'},
		//                    cssTo: {'border': '10px solid #eeeeee'},
		//                    addClass: null,   // $.addClass() param value to add classes when TSB enters rule region
		//                    removeClass: null,    // $.removeClass() param value to remove classes when TSB enters rule region
		//                    onCheckIn: function(element){ // callback on TSB enters rule region
		//                        element
		//                        .hide('fade', 100);
		//                        searchInputMain.val(searchInputTop.val());
		//                    },
		//                    onCheckOut: function(element){} // callback on TSB leaves rule region
		//                    onTopIn: function(element){}  // callback on TSB enters rule region from the top border
		//                    onTopOut: function(element){}  // callback on TSB leaves rule region from the top border
		//                    onBottomIn: function(element){}  // callback on TSB enters rule region from the bottom border
		//                    onBottomOut: function(element){}  // callback on TSB leaves rule region from the bottom border
		//                    onScroll: function(element, offset, length){}  // callback on scroll event while TSB is in the rule region
		//                                      // offset - is the offset (px) of the TSB from the rule region top border
		//                                      // length - is the rule region size (px)
		//                    onDirectionChanged: function(element, direction){}
		//                },
		//                {
		//                    from: searchFormMain.offset().top,
		//                    alias: 'searchform',
		//                    css: null,//{'display': 'block'},
		//                    addClass: null,
		//                    removeClass: null,
		//                    onCheckIn: function(element){
		//                        element.show('fade', 300);
		//                        searchInputTop.val(searchInputMain.val());
		//                    },
		//                    onCheckOut: function(element){}
		//                }
		//            ]
		//        }

	};

	scroolly._isObject = function (val) {
		return typeof val === 'object';
	};

	scroolly._isArray = function (val) {
		return val instanceof Array;
	};

	scroolly._isNumber = function (val) {
		return val instanceof Number || typeof val === 'number';
	};

	scroolly._isString = function (val) {
		return val instanceof String || typeof val === 'string';
	};

	scroolly._default = function (obj, key, defaultValue) {
		if (defaultValue === undefined) {
			defaultValue = null;
		}
		var parts = (key + '').split('.');
		if (obj && (scroolly._isObject(obj) || scroolly._isArray(obj))) {
			var root = obj,
				part;
			for (var i in parts) {
				part = parts[i];
				if ((scroolly._isObject(root) || scroolly._isArray(root)) && root[part] !== undefined) {
					root = root[part];
				} else {
					return defaultValue;
				}
			}
			return root;
		}

		return defaultValue;
		//        return _.empty(obj[key])?defaultValue:obj[key];
	};

	/**
	 * Parse rule boundry
	 * @param {string} boundry - '[anchor] [offset] = [vieport anchor] [offset]'
	 * @return {object} - parsed boundry
	 */
	scroolly.parseCoords = function (boundry) {
		var strings = boundry.split(/\s*=\s*/),
			coordRel = strings[0] || 'doc-top',
			parsedCoordRel = scroolly.parseCoord(coordRel),
			coordVP = strings[1] || parsedCoordRel.anchor,
			parsedCoordVP = scroolly.parseCoord(coordVP);

		return [parsedCoordRel, parsedCoordVP];
	};

	/**
	 * Parse rule coord part
	 * @param {string} coord - '[anchor] [offset]'
	 * @return {object} - parsed boundry
	 */
	scroolly.parseCoord = function (coord) {
		var reAnchor = /((vp|doc|el|con)-)?(top|center|bottom)?/i,
			reOffsetStr = '(\\+|-)?\\s*(\\d+)(\\%|vp|doc|el|con)?',
			reOffset = new RegExp(reOffsetStr, 'gi'),
			mA = coord.match(reAnchor),
			mO = coord.match(reOffset);

		if (!mA && !mO) {
			return false;
		}

		var subject = mA[1] ? mA[2] : 'vp',
			anchor = mA[3] || 'top',
			offsets = [];

		if (mO) {
			reOffset = new RegExp(reOffsetStr, 'i');
			var offsetStr,
				mO2,
				sign,
				offset,
				offsetSubject;

			for (var i = 0; i < mO.length; i++) {
				offsetStr = mO[i];
				mO2 = offsetStr.match(reOffset);
				sign = mO2[1] && mO2[1] === '-' ? -1 : 1;
				offset = mO2[2] && parseInt(mO2[2]) * sign || 0;
				offsetSubject = 'px';

				if (mO2[3]) {
					offsetSubject = mO2[3] === '%' ? subject : mO2[3];
				}

				offsets.push({
					offset: offset,
					subject: offsetSubject
				});
			}
		}
		return {
			original: coord,
			subject: subject,
			anchor: anchor,
			offsets: offsets
		};

	};

	/**
	 * Calculate coord position towards top of the document
	 * @param {string} coord - '[anchor] [offset]'
	 * @param {jQuery(element)} $element
	 * @param {jQuery(container)} $container
	 * @return {object} - parsed boundry
	 */
	scroolly.calculateCoord = function (coord, $element, $container) {
		if (scroolly._isString(coord)) {
			coord = scroolly.parseCoord(coord);
		}

		var subjectCoord = 0;
		if ('vp' === coord.subject) {
			switch (coord.anchor) {
				case 'top':
					subjectCoord = scroolly.scrollTop;
					break;
				case 'center':
					subjectCoord = scroolly.scrollCenter;
					break;
				case 'bottom':
					subjectCoord = scroolly.scrollBottom;
					break
			}
		} else if ('doc' === coord.subject) {
			switch (coord.anchor) {
				case 'top':
					subjectCoord = 0;
					break;
				case 'center':
					subjectCoord = scroolly.docMiddle;
					break;
				case 'bottom':
					subjectCoord = scroolly.docHeight;
			}
		} else {
			var $subject = 'con' === coord.subject ? $container : $element,
				subjectHeight = $subject.outerHeight(),
				subjectTop = $subject.offset().top,
				subjectBottom = subjectTop + subjectHeight,
				subjectCenter = subjectTop + Math.floor(subjectHeight / 2);

			switch (coord.anchor) {
				case 'top':
					subjectCoord = subjectTop;
					break;
				case 'center':
					subjectCoord = subjectCenter;
					break;
				case 'bottom':
					subjectCoord = subjectBottom;
					break;
			}
		}

		var i, o, subjectOffset, relativeHeight;
		for (i = 0; i < coord.offsets.length; i++) {
			o = coord.offsets[i];
			subjectOffset = o.offset;

			if ('px' !== o.subject) {
				relativeHeight = 0;
				switch (o.subject) {
					case 'vp':
						relativeHeight = scroolly.winHeight;
						break;
					case 'doc':
						relativeHeight = scroolly.docHeight;
						break;
					case 'el':
						relativeHeight = $element.outerHeight();
						break;
					case 'con':
						relativeHeight = $container.outerHeight();
						break;
				}

				subjectOffset = Math.ceil(o.offset / 100 * relativeHeight);
				//                console.log(subjectOffset);
			}
			subjectCoord += subjectOffset;
		}

//        console.dir({'computed':{ags: arguments, res: subjectCoord}});

		return subjectCoord;
	};

	/**
	 * Calculate how much we should scroll down till boundry
	 * @param {Object} coords
	 * @param {$(DOMnode)} $element
	 * @param {$(DOMnode)} $container
	 * @returns {integer} how much we should scroll down till boundry
	 */
	scroolly.cmpCoords = function (coords, $element, $container) {
		return scroolly.calculateCoord(coords[0], $element, $container) - scroolly.calculateCoord(coords[1], $element, $container);
	};

	/**
	 * Check if rule is active
	 * @param {object} rule
	 * @return {boolean}
	 */
	scroolly.isRuleInActiveWidthRange = function (rule) {
		var fromX = scroolly._default(rule, 'minWidth', 0),
			toX = scroolly._default(rule, 'maxWidth', 'infinity'),
			meter = scroolly._default(scroolly.options, 'meter'),
			width = $(window).width(),
			minWidthScrolly,
			maxWidthScrolly,
			checkinWidth;

		if (meter.length) {
			minWidthScrolly = meter.length ? parseInt(meter.css('min-width')) : 0;
			maxWidthScrolly = meter.length ? meter.css('max-width') : 'none';
			maxWidthScrolly = maxWidthScrolly === 'none' ? 'infinity' : parseInt(maxWidthScrolly);
			checkinWidth = fromX <= minWidthScrolly && (toX === 'infinity' || toX >= maxWidthScrolly);

			return checkinWidth;
		}

		return fromX < width && (toX === 'infinity' || toX >= width);
	};

	/**
	 * Check if rule is active
	 *
	 * @param {object} rule
	 * @param {$(DOMnode)} $element
	 * @param {$(DOMnode)|String} $container description
	 * @returns {boolean|object} false if rule is not active or scrolling params instead
	 * {
	 *      offset: how many pixels since top boundry were scrolled
	 *      length: total length of the region in pisels
	 * }
	 */
	scroolly.isRuleActive = function (rule, $element, $container) {
		var checkinWidth = scroolly.isRuleInActiveWidthRange(rule);
		if (!checkinWidth) {
			return false;
		}

		var ruleDirection = scroolly._default(rule, 'direction', 0),
			scrollDirection = scroolly.direction;

		if (ruleDirection && (ruleDirection > 0 && scrollDirection < 0 || ruleDirection < 0 && scrollDirection >= 0)) {
			return false;
		}

		var fromY = scroolly._default(rule, 'from', '0'),
			toY = scroolly._default(rule, 'to', 'finish');

		var toTop = scroolly.cmpCoords(fromY, $element, $container);
		if (toTop > 0) {
			return false;
		}

		var toBottom = scroolly.cmpCoords(toY, $element, $container);
		if (toBottom <= 0) {
			return false;
		}

		return {
			offset: -toTop,
			length: toBottom - toTop
		};
	};

	/**
	 * Add ellement with its rules to scroll layout
	 * See the commented sample above for the rules syntax
	 *
	 * @param {string} id
	 * @param {$(DOMnode)} $element
	 * @param {array} rules
	 * @param {$(DOMnode)} $container description
	 */
	scroolly.addItem = function (id, $element, rules, $container) {
		if (!$element.length) {
			return false;
		}

		$container = $container || 'self';

		var rule,
			isAbsolute,
			fromY,
			toY,
			fromCss,
			toCss,
			cssOnScroll

		for (var i in rules) {
			rule = rules[i];

			isAbsolute = !$container;//?true:false;

			fromY = scroolly._default(rule, 'from', 'doc-top');

			if (scroolly._isString(fromY) || scroolly._isNumber(fromY)) {
				fromY = scroolly.parseCoords('' + fromY);
				rule.from = fromY;
			}

			toY = scroolly._default(rule, 'to', 'doc-bottom');

			if (scroolly._isString(toY) || scroolly._isNumber(toY)) {
				toY = scroolly.parseCoords('' + toY);

				rule.to = toY;
			}

			fromCss = scroolly._default(rule, 'cssFrom');
			toCss = scroolly._default(rule, 'cssTo');

			if (fromCss && toCss) {
				cssOnScroll = function (element, offset, length, rule) {
					var progress = offset / length,
						fromCss = scroolly._default(rule, 'cssFrom'),
						toCss = scroolly._default(rule, 'cssTo'),
						css = {},
						fromProp,
						toProp;

					for (var property in fromCss) {
						fromProp = fromCss[property];
						toProp = scroolly._default(toCss, property, fromProp);
						css[property] = scroolly.getTransitionValue(fromProp, toProp, progress);
					}

					element.css(scroolly.extendCssWithPrefix(css));
				};

				rule.cssOnScroll = cssOnScroll;
			}
		}
		if ($element.length > 1) {
			$element.each(function (i) {
				var clonedRules = [],
					rule,
					clonedRule,
					$con = null;

				for (var j = 0; j < rules.length; j++) {
					rule = rules[j];
					clonedRule = {};
					$.extend(clonedRule, rule);
					clonedRules.push(clonedRule);
				}

				if ($container) {
					if ($container === 'self') {
						$con = $container;
					} else {
						$con = $container.length > 1 && i < $container.length ? $($container[i]) : $container;
					}
				}

				scroolly.addItem(id + '-' + i, $(this), clonedRules, $con);
			});

			return true;
		}
		var item = scroolly._default(scroolly.scrollLayout, id);
		if (item) {
			item.rules.concat(rules);
		} else {
			scroolly.scrollLayout[id] = {
				element: $element,
				container: $container,
				rules: rules
			};
		}
		return true;
	};

	scroolly.factory = function ($element, rules, $container, id) {
		scroolly.init();

		if (!$element.length) {
			return false;
		}

		if (!rules) {
			return false;
		}

		id = id || $element[0].tagName + '_' + Object.keys(scroolly.scrollLayout).length;
		scroolly.addItem(id, $element, rules, $container, false);
	};

	/**
	 * Fix DOM element in NON-Responsive (non viewport width dependent) layout.
	 * When applied, DOMnode is fixed when TSB is within
	 * (node's top border - offsetTop) and ($bottomContainer's bottom border - offsetBottom)
	 * and unfixed when TSB is out of the region
	 *
	 * @param string id
	 * @param $(DOMnode) $element
	 * @param object params: {
	 *      $bottomContainer - $(DOMnode) which restricts fix from the bottom,
	 *          '<body>' by default,
	 *          'next' means the next dom sibling $element.next()
	 *          'parent' means $element.parent()
	 *      mode - sets the mode of adding needed white space to $bottomContainer
	 *          when $element is fixed
	 *          'margin' means margin-top=$element.height() wil be added to $bottomContainer
	 *          'padding' means padding-top=$element.height() wil be added to $bottomContainer
	 *      offsetTop - top offset that is left before fixed element when fixed
	 *      offsetBottom - bottom offset left before $bottomContainer
	 *      minWidth, maxWidth - viewport width (px) boundries
	 *          is used within stickItemXY for responsive layouts
	 *          0, 'infinity' by default
	 *      static -
	 * }
	 */
	scroolly.stickItem = function (id, $element, params /*$bottomContainer, mode, offsetTop, offsetBottom*/) {
		scroolly.stickItemXY(id, $element, (params instanceof Array) ? params : [params]);
	};

	/**
	 * Fix DOM element in NON-Responsive (non viewport width dependent) layout.
	 * When applied, DOMnode is fixed when TSB is within
	 * (node's top border - offsetTop) and ($bottomContainer's bottom border - offsetBottom)
	 * and unfixed when TSB is out of the region
	 *
	 * @param string id
	 * @param $(DOMnode) $element
	 * @param array params - array of objects described in stickItem()
	 */
	scroolly.stickItemXY = function (id, $element, params /*$bottomContainer, mode, offsetTop, offsetBottom*/) {
		params = params || [];
		var rules = [],
			xRange,
			$bottomContainer,
			mode,
			offsetTop,
			offsetBottom,
			minWidth,
			maxWidth,
			isStatic
		;
		for (var x in params) {
			xRange = params[x];
			$bottomContainer = scroolly._default(xRange, '$bottomContainer', $('body'));
			mode = scroolly._default(xRange, 'mode');
			offsetTop = scroolly._default(xRange, 'offsetTop', 0);
			offsetBottom = scroolly._default(xRange, 'offsetBottom', 0);
			minWidth = scroolly._default(xRange, 'minWidth', 0);
			maxWidth = scroolly._default(xRange, 'maxWidth', 'infinity');
			isStatic = scroolly._default(xRange, 'static', false);

			if ('next' === $bottomContainer) {
				mode = mode || 'margin';
				$bottomContainer = $($element).next();
			} else if ('parent' === $bottomContainer || !$bottomContainer) {
				mode = mode || 'padding';
				$bottomContainer = $($element).parent();
			}

			if (!isStatic) {
				rules.push({
					source: 'sticky',
					alias: 'top',
					minWidth: minWidth,
					maxWidth: maxWidth,
					offsetTop: offsetTop,
					offsetBottom: offsetBottom,
					bottomContainer: $bottomContainer,
					mode: mode
				});
				rules.push({
					source: 'sticky',
					alias: 'fixed',
					minWidth: minWidth,
					maxWidth: maxWidth,
					offsetTop: offsetTop,
					offsetBottom: offsetBottom,
					bottomContainer: $bottomContainer,
					mode: mode
				});

				rules.push({
					source: 'sticky',
					alias: 'bottom',
					minWidth: minWidth,
					maxWidth: maxWidth,
					offsetTop: offsetTop,
					offsetBottom: offsetBottom,
					bottomContainer: $bottomContainer,
					mode: mode
//                    from: offset_2,
//                    css: {'position': 'absolute', 'top':(offset_2+offsetTop)+'px'}
				});
			} else {
				rules.push({
					source: 'sticky',
					alias: 'static',
					minWidth: minWidth,
					maxWidth: maxWidth,
					bottomContainer: $bottomContainer
				});
			}
		}

		scroolly.addItem(id, $($element), rules);
	};

	/**
	 * This function calculates all rules boundries when browser is resized and
	 * enters new width range. We cannot precalculate all sizes as during window
	 * resize some element are resized.
	 *
	 * @param {$(DOMnode)} $element
	 * @param {object} rule - single rule
	 * @returns {object} - recalculated rule
	 */
	scroolly.processStickyItemRange = function ($element, rule) {
		rule = rule || {};

		var $bottomContainer = scroolly._default(rule, 'bottomContainer', $('body')),
			mode = scroolly._default(rule, 'mode'),
			offsetTop = scroolly._default(rule, 'offsetTop', 0),
			offsetBottom = scroolly._default(rule, 'offsetBottom', 0),
			itemHeight = parseInt($element.css('margin-top')) + $element.height() + parseInt($element.css('margin-bottom'));

		if ($element.css('box-sizing') === 'border-box') {
			itemHeight += parseInt($element.css('padding-top')) + parseInt($element.css('padding-bottom'));
		}

		var bottomContainerHeight = parseInt($bottomContainer.css('margin-top')) + $bottomContainer.height() + parseInt($bottomContainer.css('margin-bottom'));
		if ($bottomContainer.css('box-sizing') === 'border-box') {
			bottomContainerHeight += parseInt($bottomContainer.css('padding-top')) + parseInt($bottomContainer.css('padding-bottom'));
		}

		var offset_1 = Math.round($element.offset().top - parseInt($element.css('margin-top'))),
			offset_2 = Math.round($bottomContainer.offset().top + (bottomContainerHeight - itemHeight - offsetBottom));

		switch (rule.alias) {
			case 'top':
				rule.from = 0;
				rule.to = offset_1 - offsetTop;
				rule.css = {'position': 'absolute', 'top': offset_1 + 'px'};
				rule.itemHeight = itemHeight;
				break;

			case 'fixed':
				rule.from = offset_1 - offsetTop;
				rule.to = offset_2;
				rule.css = {'position': 'fixed', 'top': offsetTop + 'px'};
				rule.itemHeight = itemHeight;
				break;

			case 'bottom':
				rule.from = offset_2;
				rule.css = {'position': 'absolute', 'top': (offset_2 + offsetTop) + 'px'};
				rule.itemHeight = itemHeight;
				break;

			case 'static':
				rule.from = 0;
				rule.css = {'position': '', 'top': ''};
				rule.itemHeight = 0;
				break;
		}

		return rule;
	};

	/**
	 * Heads up, this function is called on window resize. However even if window
	 * has entered new width range it doesn't mean that new responsive styles were
	 * allready applied. So we cannot rely on $( window ).width(). What we can rely
	 * on are styles that are applied to some predefined element called 'meter'.
	 *
	 * Html: (our Meter)
	 * <div class="scroolly"></div>
	 *
	 * CSS:
	 *
	 * .scroolly{
	 *      display: none;
	 * }
	 *
	 * media (min-device-width : 320px) and (max-device-width : 480px){
	 *      .scroolly{
	 *          min-width: 320px;
	 *          max-width: 480px;
	 *      }
	 * }
	 * media (min-device-width : 481px) and (max-device-width : 800px){
	 *      .scroolly{
	 *          min-width: 481px;
	 *          max-width: 800px;
	 *      }
	 * }
	 *
	 * JS rules:
	 *
	 * {
	 *      minWidth: 320,
	 *      maxWidth: 480
	 * },
	 * {
	 *      minWidth: 480,
	 *      maxWidth: 800
	 * }
	 *
	 * @returns {Boolean}
	 */
	scroolly.onResize = function () {
		scroolly.winHeight = $(window).height();
		//        scroolly.docHeight = $(document).height();
		scroolly.docHeight = scroolly.body.height();
		scroolly.docMiddle = Math.floor(scroolly.docHeight / 2);

		var needScroll = false;

		for (var id in scroolly.scrollLayout) {
			// cycling through all visual elements that should react
			// to scrolling and resizing
			var item = scroolly.scrollLayout[id],
				rule,
				checkin,
				source
			;
			for (var i in item.rules) {
				rule = item.rules[i];
				checkin = scroolly.isRuleInActiveWidthRange(rule);
				needScroll |= checkin;
				if (checkin && rule.from === undefined) {
					$(item.element).css('position', '');
					$(item.element).css('top', '');
					if (rule.bottomContainer) {
						rule.bottomContainer.css('margin-top', '');
					}
					// item entered new range and should adapt
					source = scroolly._default(rule, 'source');
					if ('sticky' === source) {
						item.rules[i] = scroolly.processStickyItemRange(item.element, rule);
					}

				}
			}
		}
		if (needScroll) {
			// dark magick here do not touch this useless string
			scroolly.scrollLayout = scroolly.scrollLayout;
			setTimeout(function () {
				scroolly.onScroll(true);
			}, 0);
			//            scroolly.onScroll();
		}
		return true;
	};

	/**
	 * Helper to get progress values for onScroll handlers
	 * @param {integer} offset
	 * @param {integer} length
	 * @returns {object} progress metrics
	 */
	scroolly.getProgress = function (offset, length) {
		var relative = offset / length;
		return {
			offset: offset,
			length: length,
			relative: relative,
			left: length - offset,
			leftRelative: 1 - relative
		};
	};

	/**
	 * Get transition float value  based on start, stop and progress values
	 * @param {number} start
	 * @param {number} stop
	 * @param {float} progress
	 * @returns {Number}
	 */
	scroolly.getTransitionFloatValue = function (start, stop, progress) {
		if (progress <= 0) {
			return start;
		}

		if (progress >= 1) {
			return stop;
		}

		return start + (stop - start) * progress;
	};

	/**
	 * Get transition integer value  based on start, stop and progress values
	 * @param {number} start
	 * @param {number} stop
	 * @param {float} progress
	 * @returns {Number}
	 */
	scroolly.getTransitionIntValue = function (start, stop, progress) {
		return Math.round(scroolly.getTransitionFloatValue(start, stop, progress));
	};

	/**
	 * Get [R, G, B] array of integers for provided '#RRGGBB' or '#RGB' value
	 * @param {type} color
	 * @returns {Array}
	 */
	scroolly.hashColor2rgb = function (color) {
		var m = color.match(/^#([0-9a-f]{3})$/i);
		if (m) {
			// in three-character format, each value is multiplied by 0x11 to give an
			// even scale from 0x00 to 0xff
			return [
				parseInt(m[1].charAt(0), 16) * 0x11, parseInt(m[1].charAt(1), 16) * 0x11, parseInt(m[1].charAt(2), 16) * 0x11
			];
		} else {
			m = color.match(/^#([0-9a-f]{6})$/i);
			if (m) {
				return [
					parseInt(m[1].substr(0, 2), 16), parseInt(m[1].substr(2, 2), 16), parseInt(m[1].substr(4, 2), 16)
				];
			}
		}
		return [0, 0, 0];
	};

	/**
	 * Get '#RRGGBB' value for provided R, G, B integer values
	 * @param {integer} r
	 * @param {integer} g
	 * @param {integer} b
	 * @returns {string} #RRGGBB
	 */
	scroolly.rgb2HashColor = function (r, g, b) {
		var res = '#', c, hex;
		for (var i in arguments) {
			c = arguments[i];
			hex = c.toString(16);

			if (c < 16) {
				hex = '0' + hex;
			}

			res += hex;
		}

		return res;
	};

	/**
	 * Get transition color value  based on start, stop and progress values
	 * @param {cssColor} start
	 * @param {cssColor} stop
	 * @param {float} progress
	 * @returns {Number}
	 */
	scroolly.getTransitionColorValue = function (start, stop, progress) {
		if (progress <= 0) {
			return start;
		}

		if (progress >= 1) {
			return stop;
		}

		var startRGB = scroolly.hashColor2rgb(start),
			stopRGB = scroolly.hashColor2rgb(stop),
			r = scroolly.getTransitionIntValue(startRGB[0], stopRGB[0], progress),
			g = scroolly.getTransitionIntValue(startRGB[1], stopRGB[1], progress),
			b = scroolly.getTransitionIntValue(startRGB[2], stopRGB[2], progress);

		return scroolly.rgb2HashColor(r, g, b);
	};

	/**
	 * Get transition css value  based on start, stop and progress values
	 * @param {cssColor} start
	 * @param {cssColor} stop
	 * @param {float} progress
	 * @returns {Number}
	 */
	scroolly.getTransitionValue = function (start, stop, progress) {
		if (progress <= 0) {
			return start;
		}

		if (progress >= 1) {
			return stop;
		}

		var called = 0;
		if (scroolly._isNumber(start) && scroolly._isNumber(stop)) {
			return scroolly.getTransitionFloatValue(start, start, progress);
		}

		var re = /(\d*\.\d+)|(\d+)|(#[0-9a-f]{6})|(#[0-9a-f]{3})/gi,
			stops = ('' + stop).match(re);

		return ('' + start).replace(re, function (value, float, int, color6, color3) {
			//            console.dir({'replace callback args':arguments, stops: stops, called: called});
			var currentStop = stops[called];

			called++;
			if (int && int.length) {
				return /\d*\.\d+/.test(currentStop) ? scroolly.getTransitionFloatValue(parseFloat(value), parseFloat(currentStop), progress) : scroolly.getTransitionIntValue(parseInt(value), parseInt(currentStop), progress);
			}

			if (float && float.length) {
				return scroolly.getTransitionFloatValue(parseFloat(value), parseFloat(currentStop), progress);
			}

			if (color6 && color6.length || color3 && color3.length) {
				return scroolly.getTransitionColorValue(value, currentStop, progress);
			}

			return value;
		});
	};

	/**
	 * Function that is called while sccrolls.
	 * @param {boolean} force description
	 * @returns {boolean}
	 */
	scroolly.onScroll = function (force) {
		//        var scrollPos = $(document).scrollTop(); // Y-coord that is checked against fromY & toY
		var scrollPos = scroolly.body.scrollTop(); // Y-coord that is checked against fromY & toY

		if (!force && scrollPos === scroolly.scrollTop) {
			return false;
		}

		var prevPos = scroolly.scrollTop,
			prevDirection = scroolly.direction;

		scroolly.scrollTop = scrollPos; // Y-coord that is checked against fromY & toY
		scroolly.scrollBottom = scrollPos + scroolly.winHeight;
		scroolly.scrollCenter = scrollPos + Math.floor(scroolly.winHeight / 2);
		scroolly.direction = scrollPos - prevPos;

		var directionChanged = !(scroolly.direction === prevDirection || scroolly.direction < 0 && prevDirection < 0 || scroolly.direction > 0 && prevDirection > 0),
			item,
			totalRules,
			checkedIn,
			checkedOut,
			active,
			id, i, l, j,
			rule,
			fromX,
			toX,
			container,
			$bottomContainer,
			mode,
			itemHeight;

		for (id in scroolly.scrollLayout) {
			// cycling through all visual elements that should react
			// to scrolling and resizing
			item = scroolly.scrollLayout[id];
			totalRules = item.rules.length;
			checkedIn = [];
			checkedOut = [];
			active = [];

			for (i = 0; i < totalRules; i++) {
				rule = item.rules[i];
				fromX = scroolly._default(rule, 'minWidth', 0);
				toX = scroolly._default(rule, 'maxWidth', 'infinity');

				container = item.container === 'self' ? item.element : item.container;

				rule.checkin = scroolly.isRuleActive(rule, item.element, container);
				rule.class = rule.class || 'scroll-pos-' + (rule.alias) + ' window-width-' + fromX + '-to-' + toX;
				if (rule.checkin) {
					active.push(i);
					if (!rule.isActive) {
						rule.isActive = true;
						checkedIn.push(i);
					}
				} else if (rule.isActive) {
					rule.isActive = false;
					checkedOut.push(i);
				}
				item.rules[i] = rule;
			}

			for (j = 0; j < checkedOut.length; j++) {
				i = checkedOut[j];
				rule = item.rules[i];
				item.element.removeClass(rule.class);
				if (rule.cssOnScroll) {
					l = rule.length || 0;
					rule.cssOnScroll(item.element, scrollPos > prevPos ? l : 0, l, rule);
				}
				if (rule.onScroll) {
					l = rule.length || 0;
					rule.onScroll(item.element, scrollPos > prevPos ? l : 0, l, rule);
				}
				if (rule.onCheckOut) {
					rule.onCheckOut(item.element, rule);
				}
				if (rule.onTopOut && scrollPos < prevPos) {
					rule.onTopOut(item.element, rule);
				} else if (rule.onBottomOut && scrollPos > prevPos) {
					rule.onBottomOut(item.element, rule);
				}
			}

			for (j = 0; j < checkedIn.length; j++) {
				i = checkedIn[j];
				rule = item.rules[i];

				if (rule.css) {
					item.element.css(scroolly.extendCssWithPrefix(rule.css));
				}

				if (rule.addClass) {
					item.element.addClass(rule.addClass);
				}

				if (rule.removeClass) {
					item.element.removeClass(rule.removeClass);
				}
				item.element.addClass(rule.class);

				$bottomContainer = scroolly._default(rule, 'bottomContainer');
				mode = scroolly._default(rule, 'mode');
				itemHeight = scroolly._default(rule, 'itemHeight');

				if ($bottomContainer && mode && itemHeight) {
					$bottomContainer.css(mode + '-top', itemHeight + 'px');
				}

				if (rule.onCheckIn) {
					rule.onCheckIn(item.element, rule);
				}

				if (rule.onTopIn && scrollPos > prevPos) {
					rule.onTopIn(item.element, rule);
				} else if (rule.onBottomIn && scrollPos < prevPos) {
					rule.onBottomIn(item.element, rule);
				}

				rule.length = rule.checkin.length;
			}

			for (j = 0; j < active.length; j++) {
				i = active[j];
				rule = item.rules[i];

				if (rule.cssOnScroll) {
					rule.cssOnScroll(item.element, rule.checkin.offset, rule.checkin.length, rule);
				}

				if (rule.onScroll) {
					rule.onScroll(item.element, rule.checkin.offset, rule.checkin.length, rule);
				}

				if (directionChanged && rule.onDirectionChanged) {
					rule.onDirectionChanged(item.element, scroolly.direction, rule);
				}
			}
			scroolly.scrollLayout[id] = item;
		}

	};

	//Will be called once (when scroolly gets initialized).
	scroolly.detectCSSPrefix = function () {
		//Only relevant prefixes. May be extended.
		//Could be dangerous if there will ever be a CSS property which actually starts with "ms". Don't hope so.
		var rxPrefixes = /^(?:O|Moz|webkit|ms)|(?:-(?:o|moz|webkit|ms)-)/;

		//Detect prefix for current browser by finding the first property using a prefix.
		if (!window.getComputedStyle) {
			return;
		}

		var style = window.getComputedStyle(document.body, null);

		for (var k in style) {
			//We check the key and if the key is a number, we check the value as well, because safari's getComputedStyle returns some weird array-like thingy.
			scroolly.theCSSPrefix = (k.match(rxPrefixes) || (+k === k && style[k].match(rxPrefixes)));

			if (scroolly.theCSSPrefix) {
				break;
			}
		}

		//Did we even detect a prefix?
		if (!scroolly.theCSSPrefix) {
			scroolly.theCSSPrefix = scroolly.theDashedCSSPrefix = '';

			return;
		}

		scroolly.theCSSPrefix = scroolly.theCSSPrefix[0];

		//We could have detected either a dashed prefix or this camelCaseish-inconsistent stuff.
		if (scroolly.theCSSPrefix.slice(0, 1) === '-') {
			scroolly.theDashedCSSPrefix = scroolly.theCSSPrefix;

			//There's no logic behind these. Need a look up.
			scroolly.theCSSPrefix = ({
				'-webkit-': 'webkit',
				'-moz-': 'Moz',
				'-ms-': 'ms',
				'-o-': 'O'
			})[scroolly.theCSSPrefix];
		} else {
			scroolly.theDashedCSSPrefix = '-' + scroolly.theCSSPrefix.toLowerCase() + '-';
		}
	};

	scroolly.cssPrefix = function (key) {
		return scroolly.theDashedCSSPrefix + key;
	};

	scroolly.extendCssWithPrefix = function (cssObj) {
		var cssExt = {}, prop, re, m, newProp, val;

		for (prop in cssObj) {
			re = /^-(moz-|webkit-|o-|ms-)?/i;
			m = prop.match(re);
			newProp = prop.slice(1);
			//            console.dir({m: m});
			if (m && !m[1]) {
				val = cssObj[prop];
				cssExt[newProp] = val;
				cssExt[scroolly.cssPrefix(newProp)] = val;
				delete cssObj[prop];
			}
		}

		$.extend(cssObj, cssExt);

		return cssObj;
	};

	scroolly.now = Date.now || function () {
		return +new Date();
	};

	scroolly.getRAF = function () {
		var requestAnimFrame = window.requestAnimationFrame || window[scroolly.theCSSPrefix.toLowerCase() + 'RequestAnimationFrame'],
			lastTime = scroolly.now();

		if (false && scroolly.isMobile || !requestAnimFrame) {
			requestAnimFrame = function (callback) {
				//How long did it take to render?
				var deltaTime = scroolly.now() - lastTime,
					delay = Math.max(0, 1000 / 60 - deltaTime);

				return window.setTimeout(function () {
					lastTime = scroolly.now();
					//        scroolly.timesCalled++;
					//        scroolly.x.text(scroolly.timesCalled);
					callback();
				}, delay);
			};
		}

		return requestAnimFrame;
	};

	scroolly.getCAF = function () {
		var cancelAnimFrame = window.cancelAnimationFrame || window[scroolly.theCSSPrefix.toLowerCase() + 'CancelAnimationFrame'];

		if (scroolly.isMobile || !cancelAnimFrame) {
			cancelAnimFrame = function (timeout) {
				return window.clearTimeout(timeout);
			};
		}

		return cancelAnimFrame;

	};

	scroolly.animLoop = function () {
		scroolly.onScroll();
		scroolly.animFrame = window.requestAnimFrame(scroolly.animLoop);
	};

	scroolly.init = function (options) {
		if (scroolly.isInitialized) {
			return false;
		}
		$.extend(scroolly.options, options);
		scroolly.isMobile = scroolly._default(scroolly.options, 'isMobile', (/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera));
		scroolly.detectCSSPrefix();
		scroolly.body = $(scroolly.options.body);
		window.requestAnimFrame = scroolly.getRAF();
		window.cancelAnimFrame = scroolly.getCAF();

		scroolly.timesCalled = 0;
		$(document).ready(function () {
			$(window).resize(scroolly.onResize).resize();
			//            scroolly.body.scroll(function(){scroolly.onScroll(true);}).scroll();
			scroolly.animLoop();
		});
		scroolly.isInitialized = true;
	};

	scroolly.destroy = function () {
		window.cancelAnimFrame(scroolly.animFrame);
	};

	scroolly.factorySticky = function ($element, params, id) {
		id = id || $element[0].tagName + '_' + Object.keys(scroolly.scrollLayout).length;
		return scroolly.stickItemXY(id, $element, (params instanceof Array) ? params : [params]) ? id : false;
	};

	if (patchJQuery) {
		$.scroolly = scroolly;

		$.fn.scroolly = function (rules, $container, id) {
			scroolly.factory(this, rules, $container, id);
			return this;
		};

		/**
		 * params = [widthRange1, widthRange2, ... , widthRangeN]
		 *
		 * widthRangeN = {
		 *      $bottomContainer: $(DOMnode),   // - container that defines bottom container
		 *      mode: 'margin'||'padding', // - defines the way element height will be compensated
		 *      minWidth: 0,
		 *      maxWidth: 'infinity',
		 *      static: false // - whether element should be fixed allways for current width range
		 * }
		 *
		 *
		 * @param {type} params
		 * @param {type} id
		 * @returns {Boolean|String}
		 */
		$.fn.scroollySticky = function (params, id) {
			scroolly.init();

			if (!this.length) {
				return false;
			}

			return scroolly.factorySticky(this, params, id);
		};
	}

	return scroolly;
}));
